﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DDD.Core {
    /// <summary>
    /// カメラ コンポーネント
    /// </summary>
    /// <remarks>
    /// <see cref="Camera"/> はコンポーネントの1種で、ノードにアタッチするとそのノードはカメラとして
    /// 振る舞う事ができるようになります。
    /// カメラは観測者の位置をあらわし、そこから見た3D空間を2Dスクリーンに投影する方式を定義します。
    /// あるノードからこのカメラのカメラ座標系へ変換は、<see cref="Node.LocalToCamera"/> プロパティで取得可能です。
    /// カメラは常にローカル座標系の原点に位置し-Z軸方向(0,0,-1)を向いています。Upベクトルは常に(0,1,0)です。
    /// <see cref="NearClip"/> と <see cref="FarClip"/> はZ軸のクリップ面までの距離（&gt; 0）です。
    /// <para>
    /// <h3>投影方式</h3>
    /// カメラの投影方式は4つの中から指定可能です。
    /// <list type="bullet">
    ///   <item><see cref="ProjectionType.Perspective"/> = 透視投影</item>
    ///   <item><see cref="ProjectionType.Parallel"/> = 平行投影</item>
    ///   <item><see cref="ProjectionType.Screen"/> = スクリーン投影</item>
    ///   <item><see cref="ProjectionType.Generic"/> = 汎用</item>
    /// </list>
    /// </para>
    /// 透視投影はもっとも一般的な投影方法で、近くの者は大きく、遠くの者は小さく見えます。
    /// 透視投影の詳細についは、<see cref="SetPerspective"/> を参照してください。
    /// 平行投影は近くの物と遠くの物は同じ大きさに見えます。
    /// 平行投影の詳細については、<see cref="SetParallel"/> を参照してください。
    /// スクリーン投影は2Dの描画に適した投影方法で [0,スクリーンのピクセル数] で座標指定が行えます。
    /// スクリーン投影の詳細については、<see cref="SetScreen"/> を参照してください。
    /// 汎用は変換行列としてユーザー指定の任意の4x4行列を使用します。この投影方法はユーザーが自由に設定可能です。
    /// 汎用の詳細については、<see cref="SetGeneric"/> を参照してください。
    /// <note type="implement">
    /// スクリーン投影は現在実装されていない。はよはよ
    /// </note>
    /// <para>
    /// <h3>投影行列</h3>
    /// カメラ座標系からクリップ座標系に変換する行列を投影行列といいます。
    /// 投影方式によらず4x4の行列であらわされます。投影行列は <see cref="ProjectionMatirix"/> プロパティで取得可能です。
    /// クリップは3軸とも [-w,w]（4次元空間）でクリップされます（3次元空間であらわすと[-1,1]）。
    /// クリップ座標ではZ=-1が手前で、Z=1が奥ですが、通常あまり気にする必要はありません。
    /// </para>
    /// </remarks>
    /// <seealso cref="World"/>
    public class Camera : Component {

        #region Field
        ProjectionType type;
        float fovy;
        float aspectRatio;
        float nearClip;
        float farClip;
        Matrix4x4 projMatrix;
        #endregion

        #region Constructor

        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// 初期値は以下のとおりです。
        /// <list type="bullet">
        ///   <item><see cref="Fovy"/> = 1.0</item>
        ///   <item><see cref="AspectRatio"/> = 1.0</item>
        ///   <item><see cref="NearClip"/> = 0.1</item>
        ///   <item><see cref="FarClip"/> = 100</item>
        ///   <item><see cref="ProjectionType"/> = 平行投影</item>
        ///   <item><see cref="ProjectionMatrix"/> = 単位行列</item>
        /// </list>
        /// 従ってデフォルトのカメラは、カメラ座標系で [-1,1] の範囲で [0.1,100] までをレンダリングします。
        /// </remarks>
        public Camera () {
            this.type = ProjectionType.Parallel;
            this.fovy = 1;
            this.aspectRatio = 1;
            this.nearClip = 0.1f;
            this.farClip = 100;
            this.projMatrix = Matrix4x4.Identity;
        }
        #endregion

        #region Property

        /// <summary>
        /// 投影方式
        /// </summary>
        /// <remarks>
        /// カメラの投影方式には以下の4種類があります。
        /// <list type="bullet">
        ///   <item><see cref="ProjectionType.Perspective"/> = 投資投影</item>
        ///   <item><see cref="ProjectionType.Parallel"/> = 平行投影</item>
        ///   <item><see cref="ProjectionType.Screen"/> = スクリーン投影</item>
        ///   <item><see cref="ProjectionType.Generic"/> = 汎用</item>
        /// </list>
        /// それぞれの投影方式については <see cref="Camera"/> クラスの解説を参照してください。
        /// </remarks>
        /// <seealso cref="SetPerspective"/>
       /// <seealso cref="SetParallel"/>
        /// <seealso cref="SetScreen"/>
        /// <seealso cref="SetGeneric"/>
        public ProjectionType Type {
            get { return type; }
        }

        /// <summary>
        /// カメラのY方向視野角
        /// </summary>
        /// <remarks>
        /// 投影方式が透視投影の場合はカメラのY方向視野角 (Field of View) を度数 (degree) で返します。
        /// 平行投影の場合は視野のカメラ空間での長さ(Y方向)を返します。
        /// それ以外の投影方式の場合は未定義です。
        /// </remarks>
        public float Fovy {
            get { return fovy; }
        }

        /// <summary>
        /// アスペクト比
        /// </summary>
        /// <remarks>
        /// 視野の縦横比（横/縦）です。
        /// </remarks>
        public float AspectRatio {
            get { return aspectRatio; }
        }

        /// <summary>
        /// ニア クリップ面までの距離
        /// </summary>
        /// <remarks>
        /// ビュー空間でのカメラからニア クリップ面までの距離（&gt; 0）を返します。
        /// この平面以降が描画対象です（<see cref="NearClip"/> を含む）。
        /// </remarks>
        public float NearClip {
            get { return nearClip; }
        }

        /// <summary>
        /// ファー クリップ面までの距離
        /// </summary>
        /// <remarks>
        /// ビュー空間でのカメラからファー クリップ面までの距離（&gt; 0）を返します。
        /// この平面以前が描画対象です（<see cref="FarClip"/> を含む）。
        /// </remarks>
        public float FarClip {
            get { return farClip; }
        }

        /// <summary>
        /// 投影行列
        /// </summary>
        /// <remarks>
        /// こののカメラの4x4の投影行列を取得します。
        /// 投影行列は投影方式にかかわらず4x4の変換行列であらわされます。
        /// この投影行列はカメラ座標をクリップ座標に変換します。
        /// </remarks>
        public Matrix4x4 ProjectionMatrix {
            get { return projMatrix; }
        }

        /// <inheritdoc/>
        public override IEnumerable<Object> References {
            get {
                return base.References;
            }
        }
        #endregion

        #region Method
        /// <summary>
        /// カメラを透視投影に変更
        /// </summary>
        /// <remarks>
        /// カメラを引数で指定したパラメーターを持つ透視投影に変更します。
        /// 透視投影は近くの物が大きく、遠くの物が小さく見えるもっとも一般的な投影方式です。
        /// 4x4の投影行列は下記の値に設定されます。
        /// <latex>
        /// \hspace{32} M = \begin{bmatrix} 
        ///                   1/w &  0  &       0       &      0        \\
        ///                    0  & 1/h &       0       & -2*near*far/d \\
        ///                    0  &  0  & -(near+far)/d & -2*near*far/d \\
        ///                    0  &  0  &      -1       &      0
        ///                 \end{bmatrix}
        /// </latex>
        /// ただし、
        /// <latex>
        /// \hspace{32} \begin{eqnarray}
        ///                  h & = & \tan(fovy/2)  \\
        ///                  w & = & aspectRatio*h \\
        ///                  d & = & far - near
        ///             \end{eqnarray}
        /// </latex>
        /// </remarks>
        /// <param name="fovy">Y方向視野角 [0,180] (in degree)</param>
        /// <param name="aspectRatio">アスペクト比（横/縦）</param>
        /// <param name="near">ニア クリップ面までの距離</param>
        /// <param name="far">ファー クリップ面までの距離</param>
        /// <exception cref="ArgumentException"><paramref name="fovy"/> &lt;= 0 または <paramref name="fovy"/> &gt;= 180</exception>
        /// <exception cref="ArgumentException"><paramref name="aspectRatio"/> &lt; 0</exception>
        /// <exception cref="ArgumentException"><paramref name="near"/> &lt;= 0</exception>
        /// <exception cref="ArgumentException"><paramref name="far"/> &lt;= 0</exception>
        /// <exception cref="ArgumentException"><paramref name="near"/> &gt; <paramref name="far"/></exception>
        public void SetPerspective (float fovy, float aspectRatio, float near, float far) {
            if (fovy <= 0 || fovy >= 180) {
                throw new ArgumentException ("fovy is invalid");
            }
            if (aspectRatio <= 0) {
                throw new ArgumentException ("AspectRatio is invalid");
            }
            if (near <= 0) {
                throw new ArgumentException ("Near is invalid");
            }
            if (far <= 0) {
                throw new ArgumentException ("Far is invalid");
            }
            if (near >= far) {
                throw new ArgumentException ("Near and far are invalid");
            }
            this.type = ProjectionType.Perspective;
            this.fovy = fovy;
            this.aspectRatio = aspectRatio;
            this.nearClip = near;
            this.farClip = far;

            var h = (float)Math.Tan ((fovy/180.0f*(float)Math.PI) / 2.0f);
            var w = aspectRatio * h;
            this.projMatrix = new Matrix4x4 (1 / w, 0, 0, 0,
                                             0, 1 / h, 0, 0,
                                             0, 0, -(near + far) / (far - near), -2 * near * far / (far - near),
                                             0, 0, -1, 0);
        }

        /// <summary>
        /// カメラを平行投影に変更
        /// </summary>
        /// <remarks>
        /// このカメラを指定のパラメーターを持つ平行投影のカメラに変更します。
        /// 平行投影は近くの物も遠くの物も同じ大きさで見える投影方法です。
        /// 4x4の投影行列は下記の値に設定されます。
        /// <latex>
        /// \hspace{32} M = \begin{bmatrix} 
        ///                   2/w &  0  &   0  &      0        \\
        ///                    0  & 2/h &   0  &      0        \\
        ///                    0  &  0  & -2/d & -(near+far)/d \\
        ///                    0  &  0  &   0  &      1
        ///                 \end{bmatrix}
        /// </latex>
        /// ただし、
        /// <latex>
        /// \hspace{32} \begin{eqnarray}
        ///                  h & = & height(= fovy)  \\
        ///                  w & = & aspectRatio*h \\
        ///                  d & = & far - near
        ///             \end{eqnarray}
        /// </latex>
        /// </remarks>
        /// <param name="fovy">Y方向視野(長さ)</param>
        /// <param name="aspectRatio">アスペクト比(横/縦)</param>
        /// <param name="near">ニアクリップ面</param>
        /// <param name="far">ファークリップ面</param>
        public void SetParallel (float fovy, float aspectRatio, float near, float far) {
            if (fovy <= 0) {
                throw new ArgumentException ("H is invalid");
            }
            if (aspectRatio <= 0) {
                throw new ArgumentException ("AspectRatio is invalid");
            }
            if (near <= 0) {
                throw new ArgumentException ("Near is invalid");
            }
            if (far <= 0) {
                throw new ArgumentException ("Far is invalid");
            }
            if (near >= far) {
                throw new ArgumentException ("Near and far are invalid");
            }
            this.type = ProjectionType.Parallel;
            this.fovy = fovy;
            this.aspectRatio = aspectRatio;
            this.nearClip = near;
            this.farClip = far;
            var h = fovy;
            var w = aspectRatio * fovy;
            this.projMatrix = new Matrix4x4 (2 / w, 0, 0, 0,
                                             0, 2 / h, 0, 0,
                                             0, 0, -2 / (far - near), -(near + far) / (far - near),
                                             0, 0, 0, 1);
        }

        /// <summary>
        /// カメラをスクリーン投影に変更
        /// </summary>
        /// <remarks>
        /// このカメラを指定のパラメーターを持つスクリーン投影のカメラに変更します。
        /// スクリーン投影は2次元画像をピクセル数と等しい座標で指定する為の投影方法です。
        /// 4x4の投影行列は下記の値に設定されます。
        /// <latex>
        /// \hspace{32} M = \begin{bmatrix} 
        ///                   2/w &  0  &   0 &  tx \\
        ///                    0  & 2/h &   0 &  ty \\
        ///                    0  &  0  &  -1 &  0  \\
        ///                    0  &  0  &   1 &  1
        ///                 \end{bmatrix}
        /// </latex>
        /// ただし、
        /// <latex>
        /// \hspace{32} \begin{eqnarray}
        ///                  h  & = & height \\
        ///                  w  & = & width  \\
        ///                  tx & = & -(2*x+width)/width \\
        ///                  ty & = & -(2*y+height)/height
        ///             \end{eqnarray}
        /// </latex>
        /// <note type="implement">
        /// 現在実装されていません。ごめん
        /// </note>
        /// </remarks>
        /// <param name="x">スクリーンの左上のX座標（カメラ座標）</param>
        /// <param name="y">スクリーンの左上のY座標（カメラ座標）</param>
        /// <param name="width">スクリーンの横方向の長さ(ピクセル数)</param>
        /// <param name="height">スクリーンの縦方向の長さ(ピクセル数)</param>
        public void SetScreen (float x, float y, float width, float height) {
            throw new NotImplementedException ("Sorry");
        }

        /// <summary>
        /// カメラを汎用に変更
        /// </summary>
        /// <remarks>
        /// このカメラを汎用投影のカメラに変更します。
        /// この投影方式はユーザーが4x4の射影行列を自由に設定可能です。
        /// 射影行列が正しい行列である事を保証するのはユーザーの責任です。
        /// 少なくとも行列式が計算できる必要があります。
        /// 4x4の投影行列は <paramref name="matrix"/> に設定されます。
        /// </remarks>
        /// <param name="matrix">投影行列</param>
        /// <exception cref="ArgumentException"><paramref name="matrix"/> の行列式が0</exception>
        public void SetGeneric (Matrix4x4 matrix) {
            if (matrix.Deternimant == 0) {
                throw new ArgumentException ("Matrix is invalid");
            }
            this.type = ProjectionType.Generic;
            this.fovy = 0;
            this.aspectRatio = 0;
            this.nearClip = 0;
            this.farClip = 0;
            this.projMatrix = matrix;
        }

        /// <inheritdoc/>
        public override string ToString () {
            return string.Format ("Camera: Type={0}, Fovy={1}, AspectRatio={2}, Near={3}, Far={4}", Type, Fovy, AspectRatio, NearClip, FarClip);
        }
        #endregion



    }
}
